// AspSimulator.cs
//
// Implements HomeUX.Components.AspSimulator.
//
// See ..\..\Drivers\Asp\DocFiles\AspDriver.htm for simulator usage information.
//

using System;
using System.Globalization;
using System.IO.Ports;
using System.Linq;
using System.Text;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates a collection of one or more Sound Control Technologies ASP Audio Matrix 8x8 devices
/// connected together to implement an N-input, N-output audio router.
/// </summary>
///
/// <remarks>
/// See <n>AspDriver</n> documentation for <r>AspSimulator</r> usage information.
/// </remarks>
///
public class AspSimulator : VirtualSerialPort
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The number of milliseconds to wait before each reply is sent back to the serial port.
    /// Increasing this number slows down the speed of the simulation.
    /// </summary>
    const int LatencyMsec = 1000;

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private State
	//

	/// <summary>
	/// This object is locked before a thread accesses this object, to serialize access to the
	/// object.
	/// </summary>
	object _lock = new object();

	/// <summary>
	/// Serial port read buffer, i.e. data to send to client.
	/// </summary>
	StringBuilder _sendToClient = new StringBuilder(1000);

	/// <summary>
	/// Work queue for scheduling asynchronous operations.
	/// </summary>
	WorkQueue _workQueue;

	/// <summary>
	/// The number of ASP 8x8 units.
	/// </summary>
	int _unitCount = 1/*default*/;

	/// <summary>
	/// The number of input and output channels.
	/// </summary>
	int _channelCount;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

    /// <summary>
    /// The number of commands that have been executed.
    /// </summary>
    int _commandsExecuted;

	/// <summary>
	/// Element <c>i</c> contains the input preamp gain level for the input channel <c>i + 1</c>
	/// (1 to <r>_channelCount</r>).  Element values are 0 to 3, corrsponding to input gain levels
	/// L0 (0 dB), L1 (+10dB), H0 (+50dB), H1 (+60dB).
	/// </summary>
	InputGainLevel[] _inputGain;

	/// <summary>
	/// Element <c>(i,j)</c> contains the gain level for the crosspoint that connects input channel
	/// <c>i + 1</c> (1 to <c>_channelCount</c>) to output channel <c>j + 1</c> (1 to
	/// <r>_channelCount</r>).  Gain levels are 0 to 70 in 1 dB steps (0 is unity gain, i.e.
	/// maximum volume), or -1 for mute.
	/// </summary>
	decimal[,] _crosspointGain;

	/// <summary>
	/// Element <c>i</c> contains the master gain level for the output channel <c>i + 1</c> (1 to
	/// <c>_channelCount</c>).  Gain levels are 0 to 31 in 1.5 dB steps (0 is unity gain, i.e.
	/// maximum volume), or -1 for mute.
	/// </summary>
	decimal[] _masterGain;

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public AspSimulator() : this(new string[0])
	{
	}

    /// <summary>
    /// Creates an instance of this class.  Allows simulator arguments to be specified.
    /// </summary>
    ///
    /// <param name="args">Simulator arguments.  See AspDriver.htm for details.</param>
    ///
    public AspSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Units":
                    _unitCount = System.Xml.XmlConvert.ToInt32(argValue);
                    if (_unitCount <= 0)
                        throw new ArgumentException();
                    break;
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }

		// initialize <_channelCount> based on <_unitCount>
		_channelCount = _unitCount * 8;
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
		lock (_lock)
		{
			base.Dispose();
			if (_workQueue != null)
			{
				_workQueue.Dispose();
				_workQueue = null;
			}
		}
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// VirtualSerialPort Overrides
	//

    /// <summary>
    /// Overrides <r>VirtualSerialPort.SupportsLineBasedTextMode</r>.
    /// </summary>
    ///
	public override bool SupportsLineBasedTextMode
	{
		// this virtual serial port only supports line-based text mode
		get
		{
			return true;
		}
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.NewLine</r>.
    /// </summary>
    ///
    public override string NewLine
	{
		// note that NewLine refers to the line terminator that the device expects to receive, not
		// the terminator it necessarily sends; in the case of ASP, it expects to receive CR but
		// sends CRLF
		get
		{
            return "\r";
		}
        set
        {
            if (value != "\r")
            {
                throw new InvalidOperationException(
					"ASP expects lines it receives to be terminated by CR");
			}
        }
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.Open</r>.
    /// </summary>
    ///
    public override void Open()
    {
        lock (_lock)
        {
			// initialize state
			InitializeState();

            // create some fake state
            if (_channelCount >= 8)
            {
                _masterGain[0] = 12.5M; // e.g. Kitchen
                _masterGain[1] = 12.5M; // e.g. Kitchen
                _masterGain[6] = 7M; // e.g. Game Room
                _masterGain[7] = 7M; // e.g. Game Room
                _crosspointGain[4, 0] = 1; // e.g. Tivo->Kitchen
                _crosspointGain[5, 1] = 1; // e.g. Tivo->Kitchen
            }
            if (_channelCount >= 16)
            {
                _masterGain[8] = 9.5M; // e.g. Laundry Room
                _masterGain[10] = 7M; // e.g. Game Room
                _masterGain[11] = 7M; // e.g. Game Room
                _crosspointGain[4, 8] = 1; // e.g. Tivo->Laundry Room
                _crosspointGain[5, 8] = 1; // e.g. Tivo->Laundry Room
                // test having more than two inputs connected to an output
                _crosspointGain[16, 8] = 1; // e.g. Music Server->Laundry Room
                _crosspointGain[17, 8] = 1; // e.g. Music Server->Laundry Room
            }
            if (_channelCount >= 24)
            {
                _crosspointGain[16, 6] = 1; // e.g. Music Server->Game Room
                _crosspointGain[17, 7] = 1; // e.g. Music Server->Game Room
                _crosspointGain[16, 10] = 1; // e.g. Music Server->Game Room
                _crosspointGain[17, 11] = 1; // e.g. Music Server->Game Room
            }

			// create a work queue for queueing events
			_workQueue = new WorkQueue("AspSimulator");

			// debugging feedback
			Trace("Initialized");

            // misbehave if simulator argument Test=1 was specified
            if (_test == 1)
            {
                _sendToClient.Append("RandomJunk\r\nFromAspSimulator");
                for (int unitId = 1; unitId <= _unitCount; unitId++)
                {
                    int unitId2 = unitId; // fix delegate scoping problem
                    _workQueue.Add(unitId * 3000, delegate()
                    {
                        WriteToClient("!GOOD={0}\r\n", unitId2);
                    });
                }
            }
		}
    }

    /// <summary>
    /// Overrides <r>VirtualSerialPort.ReadExisting</r>.
    /// </summary>
    ///
    public override string ReadExisting()
	{
		lock (_lock)
		{
			string text = _sendToClient.ToString();
			_sendToClient.Length = 0;
			return text;
		}
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.WriteLine</r>.
    /// </summary>
    /// 
    /// <param name="text">See <r>VirtualSerialPort.WriteLine</r>.</param>
    ///
    public override void WriteLine(string text)
	{
		// client sent us a command (<text>)
		lock (_lock)
		{
			try
			{
				Trace("Received command: {0}", text);
				ExecuteAspCommand(text);
			}
			catch (AspInvalidCommandException)
			{
                // if the command wasn't understood, behave the way the real hardware behaves,
                // i.e. send back "?"
				WriteToClient("?");
			}
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Properties & Methods
	//

	/// <summary>
	/// Initializes (or reinitializes) the state of the simulated hardware.
	/// </summary>
	///
	void InitializeState()
	{
		lock (_lock)
		{
			// initialize arrays
			_inputGain = new InputGainLevel[_channelCount];
			_crosspointGain = new decimal[_channelCount, _channelCount];
			_masterGain = new decimal[_channelCount];

            // mute master outputs (since that's what "RESTORE=1" needs to do)
			for (int iOutput = 0; iOutput < _channelCount; iOutput++)
				_masterGain[iOutput] = -1;

            // mute crosspoints (since that's what "RESTORE=1" needs to do)
			for (int iOutput = 0; iOutput < _channelCount; iOutput++)
			{
				for (int iInput = 0; iInput < _channelCount; iInput++)
					_crosspointGain[iInput, iOutput] = -1;
			}
		}
	}

	/// <summary>
	/// Executes an ASP command sent from the client, after a short delay.
	/// </summary>
	///
	/// <param name="command">The ASP command to execute, not including the terminating carriage
	/// 	return.  For example, "MASTER=1,M".</param>
	///
    void ExecuteAspCommand(string command)
    {
        _workQueue.Add(500, delegate
        {
            ExecuteAspCommand2(command);
        });
    }
    
    /// <summary>
    /// Executes an ASP command sent from the client.
    /// </summary>
    ///
    /// <param name="command">The ASP command to execute, not including the terminating carriage
    /// 	return.  For example, "MASTER=1,M".</param>
    ///
    void ExecuteAspCommand2(string command)
	{
		lock (_lock)
		{
			// keep track of the number of commands that have been executed
			_commandsExecuted++;

            // if the command produces a result, it's written to <result>
            StringBuilder result = null;

			// convert the command to uppercase, since it's treated as
			// uppercase by the ASP and even echoes in uppercase
			command = command.ToUpper();

			// perform the initial echo of <command> (without a newline) --
			// this happens even if the command is invalid
            if ((_test == 1) && (_commandsExecuted == 1) && (command.Length >= 2))
            {
                // in test 1 mode, we insert a notification into the echo'd command
                WriteToClient(command.Substring(0, 1) + "!HELLO\r\n" + command.Substring(1));
            }
            else
    			WriteToClient(command);

			// split the command at the "=" character into two parts in
			// <parts>, e.g. split "MASTER=1,M" into "MASTER" and "1,M"; if
			// there's no "=", there will be only one item in <parts>
			string[] parts = command.Split('=');
			if ((parts.Length < 1) || (parts.Length > 2))
				throw new AspInvalidCommandException();

			// if there are two parts, split the second part into arguments
			// in <args>, where the arguments are separated by commas,
			// otherwise set <args> to an empty array
			string[] args;
			if (parts.Length == 2)
				args = parts[1].Split(',');
			else
				args = new string[0];

			// interpret the command
			int iInput, iOutput; // 0 to (_channelCount - 1)
			decimal gain; // 0 to max. gain level, or -1 for mute
			switch (parts[0])
			{

			case "INGAIN":

				// e.g. "INGAIN=2,L0"
				if ((parts.Length != 2) || (args.Length != 2))
					throw new AspInvalidCommandException();
				iInput = ParseChannelNumber(args[0]);
                InputGainLevel inputGain = ParseInputGainLevel(args[1]);
                _inputGain[iInput] = inputGain;
                Trace("Input #{0} gain set to {1}", iInput + 1, inputGain);
                break;

			case "ALLINGAIN":

				// e.g. "ALLINGAIN=L1,L0,L0,L0,L0,L0,L1,L0"
				if ((parts.Length != 2) || (args.Length != _channelCount))
					throw new AspInvalidCommandException();
                _inputGain = args.Select(s => ParseInputGainLevel(s)).ToArray();
                for (iInput = 0; iInput < _inputGain.Length; iInput++)
                    Trace("Input #{0} gain set to {1}", iInput + 1, _inputGain[iInput]);
				break;

			case "GAIN":

				// e.g. "GAIN=2,5" or "GAIN=2,5,M"
				if (parts.Length != 2)
					throw new AspInvalidCommandException();
                if (args.Length == 2)
                {
                    // request mode, e.g. "GAIN=2,5"
                    iInput = ParseChannelNumber(args[0]);
                    iOutput = ParseChannelNumber(args[1]);
                    gain = _crosspointGain[iInput, iOutput];
                    result = new StringBuilder(20);
                    result.AppendFormat(CultureInfo.InvariantCulture, "GAIN={0},{1},{2}",
                        iInput + 1, iOutput + 1,
                        ((gain < 0) ? "M" : gain.ToString(CultureInfo.InvariantCulture)));
                }
                else
                if (args.Length == 3)
                {
                    // set mode, e.g. "GAIN=2,5,M"
				    iInput = ParseChannelNumber(args[0]);
				    iOutput = ParseChannelNumber(args[1]);
				    gain = ParseCrosspointGain(args[2]);
				    _crosspointGain[iInput, iOutput] = gain;
                    Trace("Crosspoint (#{0},#{1}) gain set to {2}",
                        iInput + 1, iOutput + 1, FormatOutputGain(gain));
                }
                else
                    throw new AspInvalidCommandException();
                break;

			case "ALLGAIN":

				// e.g. "ALLGAIN=1" or "ALLGAIN=2,0,0,0,0,M,0,0,0"
				if (parts.Length != 2)
					throw new AspInvalidCommandException();
                if (args.Length == 1)
                {
                    // request mode, e.g. "ALLGAIN=1" -- response is e.g. "ALLGAIN=1,,,17,,,0,",
                    // where empty slots indicate mute
                    iInput = ParseChannelNumber(args[0]);
                    result = new StringBuilder(100);
                    RequestAllGain(iInput, result);
                }
                else
                if (args.Length == _channelCount)
                {
                    // set mode, e.g. "ALLGAIN=2,0,0,0,0,M,0,0,0"
                    iInput = ParseChannelNumber(args[0]);
                    for (iOutput = 0; iOutput < _channelCount; iOutput++)
                    {
                        gain = ParseCrosspointGain(args[iOutput + 1]);
                        _crosspointGain[iInput, iOutput] = gain;
                        Trace("Crosspoint (#{0},#{1}) gain set to {2}",
                            iInput + 1, iOutput + 1, FormatOutputGain(gain));
                    }
                }
                else
                    throw new AspInvalidCommandException();
                break;

			case "MASTER":

				// e.g. "MASTER=5" or "MASTER=5,M"
				if (parts.Length != 2)
					throw new AspInvalidCommandException();
                if (args.Length == 1)
                {
                    // request mode, e.g. "MASTER=5" -- response is e.g. "MASTER=5,M"
                    iOutput = ParseChannelNumber(args[0]);
                    gain = _masterGain[iOutput];
                    result = new StringBuilder(20);
                    result.AppendFormat(CultureInfo.InvariantCulture, "MASTER={0},{1}", iOutput + 1,
                        ((gain < 0) ? "M" : gain.ToString(CultureInfo.InvariantCulture)));
                }
                else
                if (args.Length == 2)
                {
                    // set mode, e.g. "MASTER=5,M"
                    iOutput = ParseChannelNumber(args[0]);
				    gain = ParseMasterGain(args[1]);
				    _masterGain[iOutput] = gain;
                    Trace("Output #{0} gain set to {1}", iOutput + 1, FormatOutputGain(gain));
                }
                else
                    throw new AspInvalidCommandException();
				break;

			case "RESTORE":

				// e.g. "RESTORE=1"
				if ((parts.Length != 2) || (args.Length != 1))
					throw new AspInvalidCommandException();
                Trace("RESTORE executed");
				InitializeState();
				break;

            case "STATUS2":

                // e.g. "STATUS2"
                if ((parts.Length != 1) || (args.Length != 0))
                    throw new AspInvalidCommandException();
                result = new StringBuilder(1000);
				for (iInput = 0; iInput < _channelCount; iInput++)
				{
                    RequestAllGain(iInput, result);
					result.Append("\r\n");
				}
                RequestAllMaster(result);
                result.Append("\r\n");
                result.Length -= 2; // remove final CRLF
                break;

			default:

				// unknown command
				throw new AspInvalidCommandException();

			}

            // misbehave for the first command if simulator argument Test=1 was specified
            if ((_test == 1) && (_commandsExecuted == 1))
            {
                // cause a driver timeout by not sending a response to the first successful
                // command
            }
            else
            {
                if (result != null)
                {
                    // the command succeeded and has a result, so write "<CR><LF>", followed by the
                    // result, followed by "<CR><LF><CR>@" to copy the observed behavior of the
                    // hardware
                    WriteToClient("\r\n{0}\r\n\r@{1}", result,
                        (_test == 1) ? "Asp\r\nExtra\r\nStuff" : String.Empty);
                }
                else
                {
                    // the command succeeded but has no result, so write "<CR><LF>", followed by
					// the command again, followed by "<CR><LF><CR>@" to copy the observed behavior
					// of the real hardware
                    WriteToClient("\r\n{0}\r\n\r@{1}", command,
                        (_test == 1) ? "Asp\r\nMore\r\nStuff" : String.Empty);
                }
            }
		}
	}

    /// <summary>
    /// Appends the result of an "ALLMASTER" request to a given <n>StringBuilder</n>.
    /// </summary>
    ///
    /// <param name="result">Where to append the result.</param>
    ///
    void RequestAllMaster(StringBuilder result)
    {
        result.Append("ALLMASTER=");
        for (int iOutput = 0; iOutput < _channelCount; iOutput++)
        {
            if (iOutput > 0)
                result.Append(",");
            decimal gain = _masterGain[iOutput];
            if (gain >= 0)
                result.AppendFormat(CultureInfo.InvariantCulture, "{0}", gain);
        }
    }

    /// <summary>
    /// Appends the result of an "ALLGAIN" request to a given <n>StringBuilder</n>.
    /// </summary>
    ///
    /// <param name="iInput">The input channel to request crosspoint gain values for.</param>
    ///
    /// <param name="result">Where to append the result.</param>
    ///
    void RequestAllGain(int iInput, StringBuilder result)
    {
        result.AppendFormat("ALLGAIN={0}", iInput + 1);
        for (int iOutput = 0; iOutput < _channelCount; iOutput++)
        {
            result.Append(",");
            decimal gain = _crosspointGain[iInput, iOutput];
            if (gain >= 0)
                result.AppendFormat(CultureInfo.InvariantCulture, "{0}", gain);
        }
    }

	/// <summary>
	/// Converts a 1-based string channel number to a 0-based integer channel number in the range
	/// 0 to <c>_channelCount-1</c>,
	/// inclusive.
	/// </summary>
	///
	/// <remarks>
	/// On error, a <c>AspInvalidCommandException</c> is thrown.
	/// </remarks>
	///
	/// <param name="channelNumber">The input or output channel number to convert.</param>
	///
	int ParseChannelNumber(string channelNumber)
	{
		lock (_lock)
		{
			try
			{
				int iChannel = int.Parse(channelNumber) - 1;
				if ((iChannel < 0) || (iChannel >= _channelCount))
					throw new AspInvalidCommandException();
				return iChannel;
			}
			catch (FormatException)
			{
				throw new AspInvalidCommandException();
			}
		}
	}

	/// <summary>
	/// Converts an input gain level "L0" (0 dB), "L1" (+10dB), "H0" (+50dB), or "H1" (+60dB) to a
	/// corresponding <r>InputGainLevel</r>.
	/// </summary>
	///
	/// <param name="level">The input gain level to convert.</param>
	///
	/// <remarks>
	/// On error, a <c>AspInvalidCommandException</c> is thrown.
	/// </remarks>
	///
	static InputGainLevel ParseInputGainLevel(string level)
	{
		// static -- no lock required
		switch (level)
		{
		case "L0":
			return InputGainLevel.L0;
		case "L1":
			return InputGainLevel.L1;
		case "H0":
			return InputGainLevel.H0;
		case "H1":
			return InputGainLevel.H1;
		default:
			throw new AspInvalidCommandException();
		}
	}

	/// <summary>
	/// Converts a string gain level between 0 and <pr>maxGain</pr>, inclusive to a <n>decimal</n>.
	/// "M" (mute) is converted to -1.
	/// </summary>
	///
	/// <param name="level">The input gain level to convert.</param>
	///
	/// <param name="maxGain">The maximum allowed value of <pr>level</pr>.</param>
	///
	/// <remarks>
	/// On error, a <c>AspInvalidCommandException</c> is thrown.
	/// </remarks>
	///
	static decimal ParseGain(string level, decimal maxGain)
	{
		// static -- no lock required
		try
		{
			if (level == "M")
				return -1;
			decimal gain = decimal.Parse(level,
				System.Globalization.CultureInfo.InvariantCulture);
			if ((gain < 0) || (gain > maxGain))
				throw new AspInvalidCommandException();
			return gain;
		}
		catch (FormatException)
		{
			throw new AspInvalidCommandException();
		}
	}

	/// <summary>
	/// Converts a string crosspoint gain level between 0 and 70, inclusive to a <n>decimal</n>
	/// value.  "M" (mute) is converted to -1.
	/// </summary>
	///
	/// <param name="level">The crosspoint gain level to convert.</param>
	///
	/// <remarks>
	/// On error, a <c>AspInvalidCommandException</c> is thrown.
	/// </remarks>
	///
	static decimal ParseCrosspointGain(string level)
	{
		// static -- no lock required
		return ParseGain(level, 70);
	}

	/// <summary>
	/// Converts a string master gain level between 0 and 31, inclusive to a <n>decimal</n> value.
	/// "M" (mute) is converted to -1.
	/// </summary>
	///
	/// <param name="level">The master gain level to convert.</param>
	///
	/// <remarks>
	/// On error, a <c>AspInvalidCommandException</c> is thrown.
	/// </remarks>
	///
	static decimal ParseMasterGain(string level)
	{
		// static -- no lock required
		return ParseGain(level, 31);
	}

	/// <summary>
	/// Formats a string and sends it string to the client asynchronously.  The client will receive
	/// the string on the virtual serial port.
	/// </summary>
	/// 
	/// <param name="format">The error message format string.  If <pr>args</pr> is empty,
	/// 	<pr>format</pr> is used without formatting.</param>
	///
	/// <param name="args">Formatting arguments.</param>
	///
	void WriteToClient(string format, params object[] args)
	{
		lock (_lock)
		{
			WriteToClient((args.Length == 0) ? format : String.Format(format, args));
		}
	}

	/// <summary>
	/// Sends a string to the client asynchronously.  The client will receive the string on the
	/// virtual serial port.
	/// </summary>
	///
	void WriteToClient(string text)
	{
		lock (_lock)
			_sendToClient.Append(text);
		_workQueue.Add(delegate
		{
            FireDataReceived(SerialData.Chars);
		});
	}

    /// <summary>
    /// Converts an output gain level (in decibels, or -1 for mute) to a human-readable string.
    /// </summary>
    ///
    /// <param name="gain">The output gain level.</param>
    ///
    string FormatOutputGain(decimal gain)
    {
        if (gain < 0)
            return "mute";
        else
            return String.Format("{0} dB", gain);
    }

	/// <summary>
	/// Writes a formatted string to the debug output window.
	/// </summary>
	///
	static void Trace(string format, params object[] args)
	{
		// static -- no lock required
		Util.Trace("AspSimulator: {0}", Util.Format(format, args));
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Nested Types
	//

	/// <summary>
	/// Indicates an invalid Asp command.
	/// </summary>
	///
	class AspInvalidCommandException : Exception
	{
	}
}

/// <summary>
/// An input gain level.
/// </summary>
enum InputGainLevel
{
	/// <summary>
	/// Input level "L0" (0 dB).
	/// </summary>
	L0,

	/// <summary>
	/// Input level "L1" (+10dB).
	/// </summary>
	L1,

	/// <summary>
	/// Input level "H0" (+50dB).
	/// </summary>
	H0,

	/// <summary>
	/// Input level "H1" (+60dB).
	/// </summary>
	H1
}

}

